﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Mathematics._Operations;
using Mathematics.Expressions;

namespace Mathematics.Parser
{
    public static class MathParser
    {

        #region Parsing Methods

        public static IEnumerable<MathExpression> Parse(IEnumerable<TokenString> tokens)
        {
            return tokens.Select(ParseExpression);
        }

        public static Domain ParseDomain(IEnumerable<TokenString> tokens)
        {
            return ParseDomain(tokens, Domain.RootDomain);
        }

        internal static Domain ParseDomain(IEnumerable<TokenString> tokens,Domain domain)
        {
            var problemDomain = Domain.EnterDomain();
            foreach (var t in tokens)
            {
                var expression = ParseExpression(t, problemDomain);
                problemDomain.AddExpression(expression);
            }
            Domain.ExitDomain();
            return problemDomain;
        }

        public static MathExpression ParseExpression(TokenString tokens)
        {
            return ParseExpression(tokens, Domain.RootDomain);
        }

        internal static MathExpression ParseExpression(TokenString tokens, Domain domain)
        {
            ParseKeywords(tokens, domain);

            ParseFunctionAssignment(tokens, domain);

            ParseGroupings(tokens, domain);

            ParseConstants(tokens);

            ParseVariableUsage(tokens, domain);

            ParseFunctionCalls(tokens, domain);

            ParseOperators(tokens);

            

            ParseVariableAssignments(tokens, domain);

            // At this point, the token string should be reduced to only a single expression
            // return this expression
            if (tokens.Count > 1)
                throw new SyntaxException("Order of operations could not be resolved");

            var firstToken= tokens.First.Value as ExpressionToken;
            if (firstToken == null)
                throw new SyntaxException("Token String contains unprocessed expressions");

            return firstToken.Expression;
        }

        #endregion

        #region Parsing Helper Methods

        private static void ParseKeywords(TokenString tokens, Domain domain)
        {
            switch (tokens.First.Value.Type)
            {
                case TokenType.PrintKeyword:
                    HandlePrintCommand(tokens, domain);
                    break;
                case TokenType.PrintFKeyword:
                    HandlePrintFCommand(tokens, domain);
                    break;
                case TokenType.ImportKeyword:
                    HandleImportCommand(tokens);
                    break;
            }
        }

        private static void ParseFunctionAssignment(TokenString tokens, Domain domain)
        {
            var functionAssignments = tokens.Where(t => t.Type == TokenType.FunctionAssignment).ToArray();
            foreach (var token in functionAssignments)
                ProcessFunctionAssignment(tokens, token, domain);
        }

        private static void ParseVariableUsage(TokenString tokens, Domain domain)
        {
            var variableTokens = tokens.Where(t => t.Type == TokenType.VariableUsage).ToArray();
            foreach (var token in variableTokens)
                ProcessVariables(tokens, token,domain);
        }

        private static void ParseConstants(TokenString tokens)
        {
            var constantTokens = tokens.Where(t => t.Type == TokenType.NumericConstant).ToArray();
            foreach (var token in constantTokens)
            {
                ProcessConstant(tokens, token);
            }
        }

        private static void ParseFunctionCalls(TokenString tokens, Domain domain)
        {
            var functionCalls = tokens.Where(t => t.Type == TokenType.FunctionCall).ToArray();
            foreach (var token in functionCalls)
                ProcessFunctionCall(token, tokens, domain);
        }

        private static void ParseVariableAssignments(TokenString tokens, Domain domain)
        {
            var variableAssignments = tokens.Where(t => t.Type == TokenType.VariableAssignment).ToArray();
            foreach (var token in variableAssignments)
                ProcessVariableAssignment(tokens, token, domain);

            // Check that all variables and functions have been assigned values in the current domain
            if (!domain.AllVariablesAssigned())
            {
                //var unassignedVariables = domain.Variables.Where(v => !v.IsAssigned);
                //throw new SyntaxException( "Not all variables have been assigned a value before leaving the problem domain");
            }
        }

        private static void ParseOperators(TokenString tokens)
        {
            var exponents = tokens.Where(t => t.Type == TokenType.ExponentOperator).ToArray();
            var multiplyDivide = tokens.Where(t => t.Type == TokenType.MultiplyOperator || t.Type == TokenType.DivideOperator).ToArray();
            var addSubtract = tokens.Where(t => t.Type == TokenType.AddOperator || t.Type == TokenType.SubtractOperator).ToArray();
            foreach (var token in exponents)
                ProcessOperator(tokens, token);
            foreach (var token in multiplyDivide)
                ProcessOperator(tokens, token);
            foreach (var token in addSubtract)
                ProcessOperator(tokens, token);
        }

        private static void ParseGroupings(TokenString tokens, Domain domain)
        {
            Token groupBegin=null;
            do
            {
                groupBegin = tokens.FirstOrDefault(t => t.Type == TokenType.GroupingBegin);
                if (groupBegin == null)
                {
                    var groupEnd = tokens.LastOrDefault(t => t.Type == TokenType.GroupingEnd);
                    if (groupEnd != null)
                        throw new SyntaxException("')' detected with no matching ')'");
                }
                else
                    ProcessGrouping(tokens, groupBegin, domain);
            } while (groupBegin != null);
        }

        private static void ProcessGrouping(TokenString tokens, Token groupBegin, Domain domain)
        {
            var node = tokens.Find(groupBegin);
            var previousNode = node.Previous;
            var nextNode = node.Next;
            if (nextNode == null)
                throw new SyntaxException("Invalid syntax near '('");

            var groupLevel = 1;
            Token groupEnd = null;
            while (nextNode!=null)
            {
                var token = nextNode.Value;
                if (token.Type == TokenType.GroupingBegin)
                    groupLevel++;
                else if (token.Type == TokenType.GroupingEnd)
                    groupLevel--;

                if (groupLevel == 0)
                {
                    groupEnd = token;
                    break;
                }

                nextNode = nextNode.Next;
            }

            if (groupEnd==null)
                throw new SyntaxException("Invalid syntax: '(' with no closing ')'");

            var first = node.Next;
            var last = tokens.Find(groupEnd).Previous;
            var sub = tokens.Substring(first.Value,last.Value);
            tokens.Snip(groupBegin, groupEnd);

            var expression = ParseExpression(sub, domain);
            var newToken = new ExpressionToken(expression);

            // Replace grouped tokens
            if (previousNode == null)
                tokens.AddFirst(newToken);
            else
                tokens.AddAfter(previousNode, newToken);
        }

        private static void ProcessFunctionCall(Token token, TokenString tokens, Domain domain)
        {
            var suffixIndex = token.Text.IndexOf('(');
            
            var functionName = token.Text.Substring(0, suffixIndex);
            var def = domain.GetFunction(functionName);

            if (def == null)
                throw new SyntaxException("Function {0} is not defined");

            var suffixLength = token.Text.Length - suffixIndex - 2;
            var suffix = token.Text.Substring(suffixIndex+1,suffixLength);

            var parameterStrings = suffix.Split(',');
            var parameters = new List<MathExpression>();
            var paramCount = def.Parameters.Count();

            for (var i = 0; i < paramCount;i++ )
            {
                MathExpression parameter = null;
                string paramString = null;

                if (i < parameterStrings.Length)
                    paramString = parameterStrings[i];

                if (!string.IsNullOrWhiteSpace(paramString))
                {
                    var paramTokens = Lexer.LexLine(paramString);
                    parameter = ParseExpression(paramTokens, domain);
                }

                parameters.Add(parameter);
            }

            var functionCall = def.CreateCall(parameters.ToArray());
            var newToken = new ExpressionToken(functionCall);
            tokens.Replace(token, newToken);
        }

        private static void ProcessConstant(TokenString tokens, Token token)
        {
            var node = tokens.Find(token);

            ValidateAtomOrder(node);

            var value = double.Parse(token.Text);

            var constant = new Constant(value);
            var newToken = new ExpressionToken(constant);
            tokens.Replace(token, newToken);
        }

        private static void ProcessVariables(TokenString tokens, Token token,Domain domain)
        {
            var node = tokens.Find(token);

            ValidateAtomOrder(node);

            var variable = domain.SafeGetVariable(token.Text);
            var newToken = new ExpressionToken(variable);
            tokens.Replace(token, newToken);
        }

        private static void ProcessVariableAssignment(TokenString tokens, Token token, Domain domain)
        {
            var node = tokens.Find(token);

            // prior node must be nothing
            if (node.Previous != null)
                throw new SyntaxException("There may not be any expressions prior to a variable assignment");

            var next = node.Next;
            ValidateExpression(next);

            var nextExpression = ((ExpressionToken)next.Value).Expression;

            var variableMatch = Lexer.VARIABLE_MATCH.Match(token.Text);
            var variableName = variableMatch.Value;

            var variable = domain.SafeGetVariable(variableName);
            variable.IsAssigned = true;

            var newExpression = new VariableAssignment(variable, nextExpression);
            var newToken = new ExpressionToken(newExpression);
            tokens.Replace(token, newToken);
            tokens.Remove(next);
        }

        private static void ProcessFunctionAssignment(TokenString tokens, Token token, Domain domain)
        {
            var node = tokens.Find(token);

            // prior node must be nothing
            if (node.Previous != null)
                throw new SyntaxException("There may not be any expressions prior to a function assignment");

            var next = node.Next;
            

            var assignmentTokens = tokens.Substring(next.Value);
            var functionDomain = ParseDomain(new []{assignmentTokens});

            var functionMatch = Lexer.VARIABLE_MATCH.Match(token.Text);
            var functionName = functionMatch.Value;

            if (domain.HasFunction(functionName))
                throw new SyntaxException(string.Format("Function {0} is already defined", functionName));

            // extract parameters
            var suffixMatch = Lexer.FUNCTION_SUFFIX_MATCH.Match(token.Text);
            var suffixMatchValue = suffixMatch.Value;
            var suffix = suffixMatchValue.Substring(1, suffixMatchValue.Length-2);
            var parameters = new List<ParameterDefinition>();
            var defParams = suffix.Split(',');
            foreach (var rawParam in defParams)
            {
                if (string.IsNullOrWhiteSpace(rawParam))
                {
                    parameters.Add(null);
                    continue;
                }
                    

                var paramTokens = Lexer.LexLine(rawParam);
                if (paramTokens.First==null)
                {
                    parameters.Add(null);
                    continue;
                }

                var firstToken = paramTokens.First.Value;
                if (firstToken==null)
                {
                    parameters.Add(null);
                    continue;
                }

                ParameterDefinition param = null;

                if (firstToken.Type == TokenType.VariableUsage)
                    param = new ParameterDefinition(firstToken.Text);
                else if (firstToken.Type == TokenType.VariableAssignment)
                {
                    var assignment = ParseExpression(paramTokens, domain) as VariableAssignment;
                    param = new ParameterDefinition(firstToken.Text,assignment.AssignmentExpression);
                }
                else
                    throw new SyntaxException(string.Format("{0} is not a valid value for a parameter definition",firstToken.Text));

                parameters.Add(param);
            }

            var newExpression = domain.DefineFunction(functionName, functionDomain, parameters.ToArray());
            var newToken = new ExpressionToken(newExpression);
            tokens.Clear();
            tokens.AddFirst( newToken);
            
        }

        private static void ProcessOperator(TokenString tokens, Token token)
        {
            var node = tokens.Find(token);

            // validate operator order
            var prior = node.Previous;
            ValidateExpression(prior);

            var priorExpression = ((ExpressionToken) prior.Value).Expression;

            var next = node.Next;
            ValidateExpression(next);

            var nextExpression = ((ExpressionToken) next.Value).Expression;

            Operator newOperator=null;
            switch (token.Type)
            {
                case TokenType.AddOperator:
                    newOperator = new AddOperator(priorExpression, nextExpression);
                    break;
                case TokenType.SubtractOperator:
                    newOperator = new SubtractOperator(priorExpression, nextExpression);
                    break;
                case TokenType.MultiplyOperator:
                    newOperator = new MultiplyOperator(priorExpression, nextExpression);
                    break;
                case TokenType.DivideOperator:
                    newOperator = new DivideOperator(priorExpression, nextExpression);
                    break;
                case TokenType.ExponentOperator:
                    newOperator = new ExponentOperator(priorExpression, nextExpression);
                    break; 
            }

            if (newOperator == null)
                throw new InvalidOperationException("Something is wrong...  Unable to create operator type");

            // Replace tokens
            var newExpression = new ExpressionToken(newOperator);
            tokens.Replace(token, newExpression);

            // clean up by removing child tokens
            tokens.Remove(prior);
            tokens.Remove(next);
        }

        private static void ValidateExpression(LinkedListNode<Token> operand)
        {
            if (operand == null)
                throw new SyntaxException("Invalid operation usage.  Operation must not be the first or last statement in an expression");

            if (!operand.Value.IsExpression())
                throw new SyntaxException("Operation is not being performed on an atomic expression");
        }

        private static void ValidateAtomOrder(LinkedListNode<Token> node)
        {
            var prior = node.Previous;
            if (prior != null)
            {
                if (prior.Value.IsAtom())
                    throw new SyntaxException("Invalid Syntax Use.  Atomic statements may not be preceded by other atomic statements");
                
            }

            var next = node.Next;
            if (next != null)
            {
                 if (next.Value.IsAtom()|| next.Value.IsAssignment())
                     throw new SyntaxException("Invalid Syntax Use. .  Atomic statements may not be followed by other atomic statements");
                
            }
        }

        #endregion

        #region Keyword Helper Methods

        private static void HandlePrintCommand(TokenString tokens, Domain domain)
        {
            var printToken = tokens.First.Value;
            var sub = printToken.Text.Substring(5);
            var subTokens = Lexer.LexLine(sub);
            var expression = ParseExpression(subTokens, domain);
            var keyword = new PrintKeyword(expression);
            tokens.Clear();
            tokens.AddFirst(new ExpressionToken(keyword));
        }

        private static void HandlePrintFCommand(TokenString tokens, Domain domain)
        {
            var printToken = tokens.First.Value;
            var sub = printToken.Text.Substring(6);
            var subTokens = Lexer.LexLine(sub);
            var expression = ParseExpression(subTokens, domain);
            var keyword = new PrintFKeyword(expression);
            tokens.Clear();
            tokens.AddFirst(new ExpressionToken(keyword));
        }

        private static void HandleImportCommand(TokenString tokens)
        {
            var printToken = tokens.First.Value;
            var path = printToken.Text.Substring(6).Trim();

            var keyword = new ImportKeyword(path);
            tokens.Clear();
            tokens.AddFirst(new ExpressionToken(keyword));
        }

        #endregion

        
    }
}
